<?php
/** Rho validation class
*
* 
* If you need the Rho_Valid class require_once this file.
* Note that Rho_Form will load this file automatically on demand.
*
* @see Rho_Form
* @package Rho
* @author Pete
* @version 0.1
* @license http://www.opensource.org/licenses/apache2.0.php Apache License, Version 2.0.*/

/** Class for validating input.
*
* @package Rho
* @final */
final class Rho_Valid{

  private $_rules;

  private $_halt_on_first_error=FALSE;
  
  
  private $_errors=array();
  
  
  public function __construct(array & $rules, $halt_on_first=FALSE){
    $this->_rules= $rules;
    $this->_halt_on_first_error=$halt_on_first;
  }
  
  public static function & factory(array $rules, $halt_on_first=FALSE){
    return new Rho_Valid($rules, $halt_on_first);
  }
  
  public function valid(array & $data){
    $result=TRUE;

    // first  pre_filter
    foreach($this->_rules as $key=>$rule) :
     if(isset($data[$key]) ){
       $rule->preFilter($data[$key]);
     }
     else{
       if($rule->optional() ){
         unset($this->rules[$key]);
       }
       else {
         $this->_errors[$key]='valid.required';
         if($this->_halt_on_first_error) return FALSE;
         $result=FALSE;
       }
     }
    endforeach;

    // now validate
    foreach($rules as $key=>$rule) :
      if( isset($data[$key]) ){
        $valid=$rule->valid($data[$key]);
        if($valid!==TRUE){
          $this->_errors[$key]=$valid;
          if($this->_halt_on_first_error) return FALSE;
          $result=FALSE;
        }
      }
    endforeach;
    
    // if validation failed we don't do the post filter
    if(! $result){
      return FALSE;
    }    
    // post filter
    foreach($rules as $key=>$rule) :
       $rule->postFilter($data[$key]);
    endforeach;
    // everything validated
    return TRUE;
  }
  
  public function getErrors(){
    return $this->_errors();
  }

}

/** Interface that all validator classes must implement.
* @package Rho
* @subpackage Rho-valid */
interface Rho_Validator{

  /** Prefilter a value.
  *
  * Called before valid()
  * Should attempt to correct minor user errors if appropiate.
  * @param string|int|float|bool &$value The value to prefilter.*/ 
  function preFilter(& $value);
  
  /** Postfilter a value.
  * 
  * Should normalise the value. Should normally be safe to output value after calling this method.
  * Called AFTER valid() IF validation passes in valid().
  * @param string|int|float|bool &$value The value to prefilter.*/ 
  function postFilter(& $value);
  
  /** Determine if a value is valid or not.
  * @param string|int|float|bool &$value The value to validate.
  * @return bool|string TRUE if valid, otherwise an error string.
  * When testing the return, make sure to use if($validator->valid($value)===TRUE)  */ 
  function valid(& $value);
  
  /** Whether the value should be filtered before being validated.
  *  
  * Usually TRUE, so you should use controller->post() or controller->get(),
  * or if the source if not from POST or GET request, Rho::filter(),
  * for $value BEFORE calling postFilter()
  * 
  * If this returns FALSE, you should use the raw $_POST/etc. and the validator class
  * is responsable for filtering in the preFilter() method.
  * @return bool Normally TRUE.
  * @see Rho_Controller::post(), Rho_Controller::get(), Rho::filter() */
  function filter();
  
  /** Whether this validation rule is optional or not.
  * Defaults to FALSE, indicating the value must be present.
  * @return bool*/
  function optional();
  
  /** Sets whether this validation rule is optional or not.
  * @param bool $optional The new optional status - default TRUE.
  * @see optional() */
  function setOptional($optional=TRUE);
}

/** Base validator class.
* @abstract Must be extended.
* @package Rho
* @subpackage Rho-valid */
abstract class Rho_Base_Validator implements Rho_Validator{

  /** @var bool Whether this rule is optional or not.*/
  protected $optional=FALSE;

  /** trims the value.*/
  public function preFilter(& $value){
     trim($value);
  }
  
  /** Converts value to HTML by calling htmlentities.*/
  public function postFilter(& $value){
    $value=htmlentities($value, ENT_QUOTES, Rho::conf('enc') );
  }
  
  /** @abstract Must be overidden.*/
  public function valid(& $value){
    return '';
  }
  
  public function filter(){
    return TRUE;
  }
  
  public function optional(){
    return $this->optional;
  }
  
  function setOptional($optional=TRUE){
    $this->optional=$optional;
  }
}

/** Validator for single line strings.
* @package Rho
* @subpackage Rho-valid */
class RValid_String extends Rho_Base_Validator implements Rho_Validator{

  protected $min_length;
  protected $max_length;
  protected $chomp;

  /** Create a new string validation rule.
  * @param int $min_length The minimum length of the value - default 0.
  * @param int $max_length The maximum length of the value - default 255.
  * @param bool $chomp If TRUE, if value if longer than $max_length, it will be shortened to $max_length by preFilter().
  * Default is TRUE.*/
  public function __construct($min_length=0, $max_length=255, $chomp=TRUE){
    $this->min_length=$min_length;
    $this->max_length=$max_length;
    $this->chomp=$chomp;
  }

  public function preFilter(& $value){
    trim($value);
    $l=mb_strlen($value);
    if($l>$this->max_length &&  $this->chomp ){
      $value=mb_substr($value, 0, $this->max_length);
    }
  }
  
  public function valid(& $value){
    $l=mb_strlen($value);
    if($l<$this->min_length){
      return 'valid.string.minlength';
    }
    if($l>$this->max_length){
      return 'valid.string.maxlength ';
    }
    return TRUE;
  }
}

/** Validator for multiple line strings, such as from a HTML form textarea.
* @package Rho
* @subpackage Rho-valid */
class RValid_Text extends RValid_String implements Rho_Validator{

    /** Create a new string validation rule.
  * @param int $min_length The minimum length of the value - default 0.
  * @param int $max_length The maximum length of the value - default 1024.
  * @param bool $chomp If TRUE, if value if longer than $max_length, it will be shortened to $max_length by preFilter().
  * Default is TRUE.*/
  public function __construct($min_length=0, $max_length=1024, $chomp=TRUE){
    parent__construct($min_length, $max_length, $chomp);
  }
  
  /** Converts value to HTML, and then newlines to <br />.*/
  public function postFilter(& $value){
    parent::postFilter($value);
    $value=nl2br($value);
  }
  
}

/** Validator for HTML input, such as from a richedit component. 
* @package Rho
* @subpackage Rho-valid */
class RValid_HTML extends RValid_String implements Rho_Validator{

  protected $tags;
  protected $allowed_attributes;
  
  public function __construct($max_length=2048, string $tags=NULL, array $allowed_attributes=NULL){
    parent__construct(0, $max_length, FALSE);
    include_once Rho::$APP_PATH.'conf/forms.php';
    if($tags==NULL){
      $this->tags=Rho::conf('forms.allowed_tags', '<p>');
    }
    else{
      $this->tags=$tags;
    }
    if($allowed_attributes==NULL){
      $this->allowed_attributes=Rho::conf('forms.allowed_attributes', array('id') );
    }
    else {
      $this->allowed_attributes=$allowed_attributes;
    }
  }
  
  public function preFilter(& $value){
    // first non-allowed tags
    $value=strip_tags($value, $this->tags);
    // tag attibutes
    include_once Rho::$PATH.'rho/lib/strip_attributes/StripAttributes.class.php';
    $sa=new StripAttributes();
    $a->allow=& $this->allowed_attributes;
    $sa->exceptions=Rho::conf('forms.allowed_attributes_exceptions', array() );
    $value=$sa->strip($value);
    $value=trim($value);
  }
  
  public function postFilter(& $value){
    // nothing
  }
  
  /** Returns FALSE - HTML validator requires unfiltered input.*/
  public function filter(){
    return FALSE;
  }
}

/** Combination Validator that combines 2 Validators together.
*
* Note that the order is important - the first rule takes precendence.
*
* You can even chain RValid_Combi to have more than 2 combined rules - e.g.
* <code>$rule=new RValid_Combi($rule1, RValid_Combi($rule2, $rule3));</code>
*
* @package Rho
* @subpackage Rho-valid */
class RValid_Combi extends Rho_Base_Validator implements Rho_Validator{

  protected $rule1;
  protected $rule2;
  
  /** Create a new combined validator.
  * @param Rho_Validator $rule1 First validator.
  * @param Rho_Validator $rule2 Second Validator.*/
  public function __construct($rule1, $rule2){
    $this->rule1=$rule1;
    $this->rule2=$rule2;
  }
  
  public function preFilter(& $value){
     $this->rule1->preFilter($value);
     $this->rule2->preFilter($value);
  }
  
  public function postFilter(& $value){
    $this->rule1->postFilter($value);
    $this->rule2->postFilter($value);
  }
  
  public function valid(& $value){
    $valid1=$this->rule1->valid($value);
    if($valid1===TRUE){
      return $this->rule2->valid($value);
    }
    return $valid1;
  }
  
  public function filter(){
    return ($this->rule1->filter() | $this->rule2->filter() );
  }
  
  public function optional(){
    return ($this->rule1->optional() & $this->rule2->optional() );
  }
  
  function setOptional($optional=TRUE){
    $this->rule1->setOptional($optional);
    $this->rule2->setOptional($optional);
  }
}